import namespace from "../namespace";
import ArrayList from "../util/ArrayList";
/**
 *
 */
class Rectangle extends namespace.geo.Point {
  NAME = `${namespace.namespace}.geo.Rectangle`;

  constructor(x, y, w, h) {
    if (x instanceof namespace.geo.Rectangle) {
      y = x.y;
      w = x.w;
      h = x.h;
      x = x.x;
    } else if (typeof x.x === "number" && typeof x.y === "number") {
      y = x.y;
      w = x.w | x.width;
      h = x.h | x.height;
      x = x.x;
    } else if (typeof x.top === "number" && typeof x.left === "number") {
      y = x.top;
      w = x.w | x.width;
      h = x.h | x.height;
      x = x.left;
    }

    super(x, y);

    this.w = w;
    this.h = h;
  }

  /**
   * @method
   * @private
   */
  adjustBoundary() {
    if (this.bx === null) {
      return;
    }
    this.x = Math.min(Math.max(this.bx, this.x), this.bw - this.w);
    this.y = Math.min(Math.max(this.by, this.y), this.bh - this.h);
    this.w = Math.min(this.w, this.bw);
    this.h = Math.min(this.h, this.bh);
  }

  /**
   * @method
   * Resizes this Rectangle by the values supplied as input and returns this for
   * convenience. This Rectangle's width will become this.width + dw. This
   * Rectangle's height will become this.height + dh.
   * <br>
   * The method return the object itself. This allows you to do command chaining, where
   * you can perform multiple methods on the same elements.
   *
   *
   * @param {Number} dw  Amount by which width is to be resized
   * @param {Number} dh  Amount by which height is to be resized
   *
   * @return  {Rectangle} The method return the object itself
   **/
  resize(dw, dh) {
    this.w += dw;
    this.h += dh;
    this.adjustBoundary();

    return this;
  }

  /**
   * @method
   * Adds the specified padding to the rectangle's bounds. This Rectangle's width
   * will become this.width + dw. The Rectangle's height will become this.height + dh.
   * The top left corner moves -dw/2, -dh/2
   * <br>
   * The method return the object itself. This allows you to do command chaining, where
   * you can perform multiple methods on the same elements.
   *
   *
   * @param {Number} dw  Amount by which width is to be resized
   * @param {Number} dh  Amount by which height is to be resized
   *
   **/
  scale(dw, dh) {
    this.w += dw;
    this.h += dh;
    this.x -= dw / 2;
    this.y -= dh / 2;
    this.adjustBoundary();

    return this;
  }

  /**
   * @method
   * Translate the rectangle with the given x/y coordiante.
   *
   * @param {Point|Number} x the x translation or the complete point to translate
   * @param {Number} [y] the y translation. Required if x is a simple number instead of a Point
   *
   *
   * @since 5.6.0
   */
  translate(x, y) {
    var other = new namespace.geo.Point(x, y);
    this.x += other.x;
    this.y += other.y;
    this.adjustBoundary();

    return this;
  }

  /**
   * @method
   * Returns a copy of the translated rectangle
   *
   * @param {Point|Number} x the x translation or the complete point to translate
   * @param {Number} [y] the y translation. Required if x is a simple number instead of a Point
   *
   * @return {Rectangle} The new translated rectangle.
   *
   * @since 5.6.0
   */
  translated(x, y) {
    var other = new namespace.geo.Point(x, y);
    return new namespace.geo.Rectangle(
      this.x + other.x,
      this.y + other.y,
      this.w,
      this.h
    );
  }

  /**
   * Sets the parameters of this Rectangle from the Rectangle passed in and
   * returns this for convenience.<br>
   * <br>
   * The method return the object itself. This allows you to do command chaining, where
   * you can perform multiple methods on the same elements.
   *
   * @param {Rectangle} Rectangle providing the bounding values
   *
   * @return  {Rectangle} The method return the object itself
   */
  setBounds(rect) {
    this.setPosition(rect.x, rect.y);

    this.w = rect.w;
    this.h = rect.h;

    return this;
  }

  /**
   * @method
   * Returns <code>true</code> if this Rectangle's width or height is less than or
   * equal to 0.
   *
   * @return {Boolean}
   */
  isEmpty() {
    return this.w <= 0 || this.h <= 0;
  }

  /**
   * @method
   * The width of the dimension element.
   *
   * @return {Number}
   **/
  getWidth() {
    return this.w;
  }

  /**
   * @method
   * Set the new width of the rectangle.
   *
   * @param {Number} w the new width of the rectangle
   */
  setWidth(w) {
    this.w = w;
    this.adjustBoundary();

    return this;
  }

  /**
   * @method
   * The height of the dimension element.
   *
   * @return {Number}
   **/
  getHeight() {
    return this.h;
  }

  /**
   * @method
   * Set the new height of the rectangle.
   *
   * @param {Number} h the new height of the rectangle
   */
  setHeight(h) {
    this.h = h;
    this.adjustBoundary();

    return this;
  }

  /**
   * @method
   * The x coordinate of the left corner.
   *
   * @return {Number}
   **/
  getLeft() {
    return this.x;
  }

  /**
   * @method
   * The x coordinate of the right corner.
   *
   * @return {Number}
   **/
  getRight() {
    return this.x + this.w;
  }

  /**
   * @method
   * The y coordinate of the top.
   *
   *@return {Number}
   **/
  getTop() {
    return this.y;
  }

  /**
   * @method
   * The y coordinate of the bottom.
   *
   *@return {Number}
   **/
  getBottom() {
    return this.y + this.h;
  }

  /**
   * @method
   * The top left corner of the dimension object.
   *
   * @return {Point} a new namespace.geo.Point objects which holds the coordinates
   **/
  getTopLeft() {
    return new namespace.geo.Point(this.x, this.y);
  }

  /**
   * @method
   * The top center coordinate of the dimension object.
   *
   * @return {Point} a new namespace.geo.Point objects which holds the coordinates
   **/
  getTopCenter() {
    return new namespace.geo.Point(this.x + this.w / 2, this.y);
  }

  /**
   * @method
   * The top right corner of the dimension object.
   *
   * @return {Point} a new namespace.geo.Point objects which holds the coordinates
   **/
  getTopRight() {
    return new namespace.geo.Point(this.x + this.w, this.y);
  }

  /**
   * @method
   * The center left  of the dimension object.
   *
   * @return {Point} a new namespace.geo.Point objects which holds the coordinates
   **/
  getCenterLeft() {
    return new namespace.geo.Point(this.x, this.y + this.h / 2);
  }

  /**
   * @method
   * The bottom left corner of the dimension object.
   *
   * @return {Point} a new namespace.geo.Point objects which holds the coordinates
   **/
  getBottomLeft() {
    return new namespace.geo.Point(this.x, this.y + this.h);
  }

  /**
   * @method
   * The bottom center coordinate of the dimension object.
   *
   * @return {Point} a new namespace.geo.Point objects which holds the coordinates
   **/
  getBottomCenter() {
    return new namespace.geo.Point(this.x + this.w / 2, this.y + this.h);
  }

  /**
   * @method
   * The center of the dimension object
   *
   * @return {Point} a new namespace.geo.Point which holds the center of the object
   **/
  getCenter() {
    return new namespace.geo.Point(this.x + this.w / 2, this.y + this.h / 2);
  }

  /**
   * @method
   * Bottom right corner of the object
   *
   * @return {Point} a new namespace.geo.Point which holds the bottom right corner
   **/
  getBottomRight() {
    return new namespace.geo.Point(this.x + this.w, this.y + this.h);
  }

  /**
   * @method
   * Return all points of the rectangle as array. Starting at topLeft and the
   * clockwise.
   *
   * @return {ArrayList} the points starting at top/left and the clockwise
   */
  getVertices() {
    var result = new ArrayList();
    // don't change the order. We expect always that the top left corner has index[0]
    // and goes clock wise
    //
    result.add(this.getTopLeft());
    result.add(this.getTopRight());
    result.add(this.getBottomRight());
    result.add(this.getBottomLeft());

    return result;
  }

  /**
   * @method
   * Return a new namespace.geo.Rectangle which fits into this rectangle. <b>ONLY</b> the x/y coordinates
   * will be changed. Not the dimension of the given rectangle.
   *
   * @param {Rectangle} rect the rectangle to adjust
   * @return the new shifted rectangle
   */
  moveInside(rect) {
    var newRect = new namespace.geo.Rectangle(rect.x, rect.y, rect.w, rect.h);
    // shift the coordinate right/down if coordinate not inside the rect
    //
    newRect.x = Math.max(newRect.x, this.x);
    newRect.y = Math.max(newRect.y, this.y);

    // ensure that the right border is inside this rect (if possible).
    //
    if (newRect.w < this.w) {
      newRect.x = Math.min(newRect.x + newRect.w, this.x + this.w) - newRect.w;
    } else {
      newRect.x = this.x;
    }

    // ensure that the bottom is inside this rectangle
    //
    if (newRect.h < this.h) {
      newRect.y = Math.min(newRect.y + newRect.h, this.y + this.h) - newRect.h;
    } else {
      newRect.y = this.y;
    }

    return newRect;
  }

  /**
   * @method
   * Return the minimum distance of this rectangle to the given {@link Point} or
   * {link Rectangle}.
   *
   * @param {Point|Rectangle} pointOrRectangle the reference point/rectangle for the distance calculation
   */
  getDistance(pointOrRectangle) {
    var cx = this.x;
    var cy = this.y;
    var cw = this.w;
    var ch = this.h;

    var ox = pointOrRectangle.getX();
    var oy = pointOrRectangle.getY();
    var ow = 1;
    var oh = 1;

    if (pointOrRectangle instanceof namespace.geo.Rectangle) {
      ow = pointOrRectangle.getWidth();
      oh = pointOrRectangle.getHeight();
    }
    var oct = 9;

    // Determin Octant
    //
    // 0 | 1 | 2
    // __|___|__
    // 7 | 9 | 3
    // __|___|__
    // 6 | 5 | 4

    if (cx + cw <= ox) {
      if (cy + ch <= oy) {
        oct = 0;
      } else if (cy >= oy + oh) {
        oct = 6;
      } else {
        oct = 7;
      }
    } else if (cx >= ox + ow) {
      if (cy + ch <= oy) {
        oct = 2;
      } else if (cy >= oy + oh) {
        oct = 4;
      } else {
        oct = 3;
      }
    } else if (cy + ch <= oy) {
      oct = 1;
    } else if (cy >= oy + oh) {
      oct = 5;
    } else {
      return 0;
    }

    // Determine Distance based on Quad
    //
    switch (oct) {
      case 0:
        cx = cx + cw - ox;
        cy = cy + ch - oy;
        return -(cx + cy);
      case 1:
        return -(cy + ch - oy);
      case 2:
        cx = ox + ow - cx;
        cy = cy + ch - oy;
        return -(cx + cy);
      case 3:
        return -(ox + ow - cx);
      case 4:
        cx = ox + ow - cx;
        cy = oy + oh - cy;
        return -(cx + cy);
      case 5:
        return -(oy + oh - cy);
      case 6:
        cx = cx + cw - ox;
        cy = oy + oh - cy;
        return -(cx + cy);
      case 7:
        return -(cx + cw - ox);
    }

    throw "Unknown data type of parameter for distance calculation in Rectangle.getDistance(..)";
  }

  /**
   * @method
   * Determin the octant of r2 in relation to this rectangle.
   * <pre>
   *
   *    0 | 1 | 2
   *    __|___|__
   *    7 | 8 | 3
   *    __|___|__
   *    6 | 5 | 4
   * </pre>
   *
   * @param {Rectangle} r2
   *
   */
  determineOctant(r2) {
    var HISTERESE = 3; // Toleranz um diese vermieden wird, dass der Octant "8" zurückgegeben wird

    var ox = this.x + HISTERESE;
    var oy = this.y + HISTERESE;
    var ow = this.w - HISTERESE * 2;
    var oh = this.h - HISTERESE * 2;

    var cx = r2.x;
    var cy = r2.y;
    var cw = 2;
    var ch = 2;
    if (r2 instanceof namespace.geo.Rectangle) {
      cw = r2.w;
      ch = r2.h;
    }

    var oct = 0;

    if (cx + cw <= ox) {
      if (cy + ch <= oy) {
        oct = 0;
      } else if (cy >= oy + oh) {
        oct = 6;
      } else {
        oct = 7;
      }
    } else if (cx >= ox + ow) {
      if (cy + ch <= oy) {
        oct = 2;
      } else if (cy >= oy + oh) {
        oct = 4;
      } else {
        oct = 3;
      }
    } else if (cy + ch <= oy) {
      oct = 1;
    } else if (cy >= oy + oh) {
      oct = 5;
    } else {
      oct = 8;
    }

    return oct;
  }

  /**
   * @method
   * Returns the direction the point <i>p</i> is in relation to the given rectangle.
   * Util method for inherit router implementations.
   *
   * <p>
   * Possible values:
   * <ul>
   *   <li>up -&gt; 0</li>
   *   <li>right -&gt; 1</li>
   *   <li>down -&gt; 2</li>
   *   <li>left -&gt; 3</li>
   * </ul>
   * <p>
   *
   * @param {Point} other the point in relation to the given rectangle
   *
   * @return {Number} the direction from <i>r</i> to <i>p</i>
   */
  getDirection(other) {
    var current = this.getTopLeft();
    switch (this.determineOctant(other)) {
      case 0:
        if (current.x - other.x < current.y - other.y)
          return namespace.geo.Rectangle.DIRECTION_UP;
        return namespace.geo.Rectangle.DIRECTION_LEFT;
      case 1:
        return namespace.geo.Rectangle.DIRECTION_UP;
      case 2:
        current = this.getTopRight();
        if (other.x - current.x < current.y - other.y)
          return namespace.geo.Rectangle.DIRECTION_UP;
        return namespace.geo.Rectangle.DIRECTION_RIGHT;
      case 3:
        return namespace.geo.Rectangle.DIRECTION_RIGHT;
      case 4:
        current = this.getBottomRight();
        if (other.x - current.x < other.y - current.y)
          return namespace.geo.Rectangle.DIRECTION_DOWN;
        return namespace.geo.Rectangle.DIRECTION_RIGHT;
      case 5:
        return namespace.geo.Rectangle.DIRECTION_DOWN;
      case 6:
        current = this.getBottomLeft();
        if (current.x - other.x < other.y - current.y)
          return namespace.geo.Rectangle.DIRECTION_DOWN;
        return namespace.geo.Rectangle.DIRECTION_LEFT;
      case 7:
        return namespace.geo.Rectangle.DIRECTION_LEFT;
      case 8:
        if (other.y > this.y) {
          return namespace.geo.Rectangle.DIRECTION_DOWN;
        }
        return namespace.geo.Rectangle.DIRECTION_UP;
    }
    return namespace.geo.Rectangle.DIRECTION_UP;
  }

  /**
   * @method
   * Compares two rectangle objects
   *
   * @param {Rectangle} o
   *
   * @return {Boolean}
   **/
  equals(o) {
    return this.x == o.x && this.y == o.y && this.w == o.w && this.h == o.h;
  }

  /**
   * @method
   * Detect whenever the hands over coordinate is inside the rectangle.
   *
   * @param {Number/Point} iX
   * @param {Number} iY
   * @returns {Boolean}
   */
  hitTest(iX, iY) {
    if (iX instanceof namespace.geo.Point) {
      iY = iX.y;
      iX = iX.x;
    }
    var iX2 = this.x + this.getWidth();
    var iY2 = this.y + this.getHeight();
    return iX >= this.x && iX <= iX2 && iY >= this.y && iY <= iY2;
  }

  /**
   * @method
   * return true if this rectangle inside the hand over rectangle
   *
   *
   * @param {Rectangle} rect
   * @returns {Boolean}
   */
  isInside(rect) {
    return (
      rect.hitTest(this.getTopLeft()) &&
      rect.hitTest(this.getTopRight()) &&
      rect.hitTest(this.getBottomLeft()) &&
      rect.hitTest(this.getBottomRight())
    );
  }

  /**
   * @method
   * return true if this rectangle contains the hand over rectangle.
   *
   *
   * @param {Rectangle} rect
   * @returns {Boolean}
   * @since 4.7.2
   */
  contains(rect) {
    return (
      this.hitTest(rect.getTopLeft()) &&
      this.hitTest(rect.getTopRight()) &&
      this.hitTest(rect.getBottomLeft()) &&
      this.hitTest(rect.getBottomRight())
    );
  }

  /**
   * @method
   * checks whenever the rectangles has an intersection.
   *
   * @param {Rectangle} rect
   * @returns {Boolean}
   */
  intersects(rect) {
    var x11 = rect.x,
      y11 = rect.y,
      x12 = rect.x + rect.w,
      y12 = rect.y + rect.h,
      x21 = this.x,
      y21 = this.y,
      x22 = this.x + this.w,
      y22 = this.y + this.h;

    var x_overlap = Math.max(0, Math.min(x12, x22) - Math.max(x11, x21));
    var y_overlap = Math.max(0, Math.min(y12, y22) - Math.max(y11, y21));

    return x_overlap * y_overlap !== 0;
  }

  /**
   * @method
   * Merge this rectangle with the given one.
   *
   * @param {Rectangle} rect
   * @since 4.8.0
   */
  merge(rect) {
    var r = Math.max(rect.getRight(), this.getRight());
    var b = Math.max(rect.getBottom(), this.getBottom());

    this.setPosition(Math.min(this.x, rect.x), Math.min(this.y, rect.y));

    this.w = r - this.x;
    this.h = b - this.y;

    return this;
  }

  /**
   * @method
   * Returns a copy of this rectangle
   *
   *
   * @returns {Rectangle}
   * @since 5.6.0
   */
  clone() {
    return new namespace.geo.Rectangle(this.x, this.y, this.w, this.h);
  }

  /**
   * @method
   * converts the rectangle to JSON representation. required for the draw2d.io.Writer
   *
   * @returns {Object}
   */
  toJSON() {
    return {
      width: this.w,
      height: this.h,
      x: this.x,
      y: this.y
    };
  }
}

Rectangle.DIRECTION_UP = 0;
Rectangle.DIRECTION_RIGHT = 1;
Rectangle.DIRECTION_DOWN = 2;
Rectangle.DIRECTION_LEFT = 3;

namespace.geo.Rectangle = Rectangle;
